package com.gitee.fsmxm.ocon;

import com.gitee.fsmxm.ocon.constant.AppEnum;
import com.gitee.fsmxm.ocon.converter.ExcelConverter;
import com.gitee.fsmxm.ocon.converter.PowerpointConverter;
import com.gitee.fsmxm.ocon.converter.WordConverter;
import com.gitee.fsmxm.ocon.core.*;
import com.gitee.fsmxm.ocon.model.*;
import com.gitee.fsmxm.ocon.util.*;
import com.jacob.com.ComThread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author fsmxm
 */
public class DocumentConverter {

    private static final Logger LOGGER = LoggerFactory.getLogger(DocumentConverter.class);

    private final Map<AppEnum, Pool<Procedure>> poolMap;

    private final Map<AppEnum, Converter> converterMap;

    private ScheduledThreadPoolExecutor checkerExecutor;

    private DocumentConverter() {
        poolMap = new HashMap<>();

        converterMap = new HashMap<>();
        converterMap.put(AppEnum.WORD, new WordConverter());
        converterMap.put(AppEnum.EXCEL, new ExcelConverter());
        converterMap.put(AppEnum.POWERPOINT, new PowerpointConverter());
    }

    /**
     * 文件转换，会等待 直至成功获取转换进程 或 被中断
     *
     * @param source 源文件，要有文件扩展名
     * @param target 目标文件，要有文件扩展名
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public void convert(File source, File target) throws ConvertException {
        convert(source, target, null);
    }

    /**
     * 文件转换，会等待 直至成功获取转换进程 或 被中断
     *
     * @param source 源文件，要有文件扩展名
     * @param target 目标文件，要有文件扩展名
     * @param app    指定转换使用的app, null 则表示不指定
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public void convert(File source, File target, AppEnum app) throws ConvertException {
        convert(new ExtFile(source), new ExtFile(target), app);
    }

    /**
     * 文件转换，会等待 直至成功获取转换进程 或 被中断
     *
     * @param source 源文件
     * @param target 目标文件
     * @param app    指定转换使用的app, null 则表示不指定
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public void convert(ExtFile source, ExtFile target, AppEnum app) throws ConvertException {

        check(source, target);

        final AppEnum appEnum = checkAppEnum(source, target, app);

        final Converter converter = converterMap.get(appEnum);

        if (converter == null) {
            throw new ConvertException("Converter not found: " + source.getExt() + " -> " + target.getExt() + ". Try specify app");
        }

        final Pool<Procedure> pool = poolMap.get(appEnum);

        if (pool == null) {
            throw new ConvertException("Pool not found: " + source.getExt() + " -> " + target.getExt() + ". Check pool config");
        }

        Procedure procedure = null;
        try {
            procedure = pool.take();
            converter.convert(procedure, new ConParam(source, target));
        } catch (Exception e) {
            throw new ConvertException("Convert failed", e);
        } finally {
            if (procedure != null) {
                procedure.release();
            }
        }
    }

    /**
     * 文件转换，会等待指定时间获取转换线程，若获取不到则返回
     *
     * @param source 源文件
     * @param target 目标文件
     * @param time   等待获取转换进程的时间，毫秒
     * @return -1 获取转换进程失败, 1 转换成功
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public int convert(File source, File target, long time) throws ConvertException {
        return convert(new ExtFile(source), new ExtFile(target), null, time);
    }

    /**
     * 文件转换，会等待指定时间获取转换线程，若获取不到则返回
     *
     * @param source 源文件
     * @param target 目标文件
     * @param app    指定转换使用的app, null 则表示不指定
     * @param time   等待获取转换进程的时间，毫秒
     * @return -1 获取转换进程失败, 1 转换成功
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public int convert(File source, File target, AppEnum app, long time) throws ConvertException {
        return convert(new ExtFile(source), new ExtFile(target), app, time);
    }

    /**
     * 文件转换，会等待指定时间获取转换线程，若获取不到则返回
     *
     * @param source 源文件
     * @param target 目标文件
     * @param app    指定转换使用的app, null 则表示不指定
     * @param time   等待获取转换进程的时间，毫秒
     * @return -1 获取转换进程失败, 1 转换成功
     * @throws ConvertException 根据源文件后缀名匹配转换app，若不支持则抛错；源文件不存在则抛错；目标文件与源文件相同则抛错; 等待获取转换程序过程中被中断则抛错
     */
    public int convert(ExtFile source, ExtFile target, AppEnum app, long time) throws ConvertException {

        check(source, target);

        final AppEnum appEnum = checkAppEnum(source, target, app);

        final Converter converter = converterMap.get(appEnum);

        if (converter == null) {
            throw new ConvertException("Converter not found: " + source.getExt() + " -> " + target.getExt() + ". Try specify app");
        }

        final Pool<Procedure> pool = poolMap.get(appEnum);

        if (pool == null) {
            throw new ConvertException("Pool not found: " + source.getExt() + " -> " + target.getExt() + ". Check pool config");
        }

        Procedure procedure = null;
        try {
            procedure = pool.get(time, TimeUnit.MILLISECONDS);

            if (procedure == null) {
                return -1;
            }

            converter.convert(procedure, new ConParam(source, target));

            return 1;
        } catch (Exception e) {
            throw new ConvertException("Convert failed", e);
        } finally {
            if (procedure != null) {
                procedure.release();
            }
        }
    }

    private void check(ExtFile source, ExtFile target) throws ConvertException {
        final File sourceFile = source.getFile();
        final String sourcePath = sourceFile.getAbsolutePath();
        if (!sourceFile.exists() || !sourceFile.isFile()) {
            throw new ConvertException("File [" + sourcePath + "] not found or not a file.");
        }

        final File targetFile = target.getFile();
        final String targetPath = targetFile.getAbsolutePath();
        if (source.equals(target)) {
            throw new ConvertException("Target [" + targetPath + "] cannot be the same as src [" + sourcePath + "]");
        }

        final File parentFile = targetFile.getParentFile();
        if (!parentFile.exists() && !parentFile.mkdirs()) {
            throw new ConvertException("Dir [" + parentFile.getAbsolutePath() + "] create failed.");
        }
    }

    private AppEnum checkAppEnum(ExtFile source, ExtFile target, AppEnum app) throws ConvertException {
        AppEnum appEnum = app;

        if (appEnum == null) {
            appEnum = AppUtils.getSupportApp(source.getExt());
        }

        if (appEnum == null) {
            appEnum = AppUtils.getSupportApp(target.getExt());
        }

        if (appEnum == null) {
            throw new ConvertException("Not support: " + source.getExt() + " -> " + target.getExt());
        }

        checkSupport(source, target, app);

        return appEnum;
    }

    private void checkSupport(ExtFile source, ExtFile target, AppEnum app) throws ConvertException {
        int ft = 0;
        if (app == AppEnum.WORD) {
            ft = WordUtils.getFormatType(target.getExt());
        } else if (app == AppEnum.EXCEL) {
            ft = ExcelUtils.getFormatType(target.getExt());
        } else if (app == AppEnum.POWERPOINT) {
            ft = PptUtils.getFormatType(target.getExt());
        }
        if (ft == -1) {
            throw new ConvertException("Not support: " + source.getExt() + " -> " + target.getExt());
        }
    }

    private void init(List<PoolProperties> poolPropertiesList) throws ConvertException {
        ComThread.InitMTA(true);
        for (PoolProperties poolProperties : poolPropertiesList) {
            Pool<Procedure> pool = new ProcessPool(poolProperties);
            pool.init();
            final String prefix = poolProperties.getAppName().substring(0, poolProperties.getAppName().indexOf('.'));
            poolMap.put(AppEnum.getInstance(prefix), pool);
        }
        initChecker();
    }

    public void close() {
        for (Map.Entry<AppEnum, Pool<Procedure>> entry : poolMap.entrySet()) {
            entry.getValue().close();
        }
        ComThread.quitMainSTA();
        ComThread.Release();
        if (checkerExecutor != null) {
            checkerExecutor.shutdown();
        }
    }

    private void initChecker() {
        checkerExecutor = new ScheduledThreadPoolExecutor(1, new DCThreadFactory("checker-"));
        checkerExecutor.scheduleAtFixedRate(() -> {
            for (Map.Entry<AppEnum, Pool<Procedure>> entry : poolMap.entrySet()) {
                ProcessPool pool = (ProcessPool) entry.getValue();
                BlockingQueue<Procedure> diligentQueue = pool.getDiligentQueue();
                int size = diligentQueue.size();
                while (size-- > 0) {
                    try {
                        final Procedure procedure = diligentQueue.poll();
                        if (procedure == null) {
                            break;
                        }
                        if (System.currentTimeMillis() - procedure.getStartTime() < pool.getPoolProperties().getFatalErrorTime()) {
                            if (procedure.getStatus() == Procedure.STATUS_BUSY) {
                                diligentQueue.offer(procedure);
                            }
                        } else {
                            LOGGER.info("Procedure stuck，try to close it and create new procedure. stuck procedure: {}", procedure);
                            procedure.close();
                            WinTaskUtils.forceKillAll(procedure.getComponentPid());
                            pool.add();
                        }
                    } catch (Exception e) {
                        LOGGER.warn("Exception ", e);
                    }
                }

                BlockingQueue<Procedure> idelQueue = pool.getIdelQueue();
                size = idelQueue.size();
                while (size-- > 0) {
                    final Procedure procedure = idelQueue.poll();
                    if (procedure == null) {
                        break;
                    }
                    // 检测进程是否存在
                    boolean isDead = false;
                    try {
                        final Task task = WinTaskUtils.getTask(procedure.getComponentPid());
                        if (task != null && procedure.getTask().getImageName().equals(task.getImageName())) {
                            idelQueue.offer(procedure);
                            continue;
                        }
                        isDead = true;
                    } catch (IOException e) {
                        idelQueue.offer(procedure);
                    }
                    try {
                        if (isDead) {
                            LOGGER.info("Procedure is dead, try to create new procedure. dead procedure: {}", procedure);
                            pool.add();
                        }
                    } catch (UnsupportedAppException e) {
                        LOGGER.warn("Exception: ", e);
                    }
                }
            }
        }, 2, 2, TimeUnit.SECONDS);
    }


    public static class Builder {

        private List<PoolProperties> poolPropertiesList;

        private Builder() {
            poolPropertiesList = new ArrayList<>();
        }

        public static Builder builder() {
            return new Builder();
        }

        public DocumentConverter build() throws ConvertException {
            final DocumentConverter documentConverter = new DocumentConverter();
            if (poolPropertiesList.isEmpty()) {
                throw new RuntimeException("Use method poolBegin() before config pool");
            }
            for (PoolProperties poolProperties : poolPropertiesList) {
                if (poolProperties.getAppName() == null || poolProperties.getAppName().isEmpty()) {
                    throw new RuntimeException("Pool appName cannot be empty: " + poolProperties.toString());
                }
                AppUtils.checkAndGetApplication(poolProperties.getAppName());
            }
            documentConverter.init(poolPropertiesList);
            return documentConverter;
        }

        public static DocumentConverter buildDefault() throws ConvertException {
            return builder()
                    .poolBegin()
                    .appName("powerpoint.ms")
                    .initProcessNum(1)
                    .startAndKillExistProcess(true)
                    .fatalErrorTime(30000)
                    .poolBegin()
                    .appName("excel.ms")
                    .initProcessNum(1)
                    .startAndKillExistProcess(true)
                    .fatalErrorTime(30000)
                    .poolBegin()
                    .appName("word.ms")
                    .initProcessNum(1)
                    .fatalErrorTime(30000)
                    .startAndKillExistProcess(true)
                    .build();
        }

        public Builder poolBegin() {
            poolPropertiesList.add(new PoolProperties());
            return this;
        }

        public Builder appName(String appName) {
            check();
            for (PoolProperties pp : poolPropertiesList) {
                if (appName.equals(pp.getAppName())) {
                    throw new RuntimeException(appName + " pool already exists.");
                }
            }
            poolPropertiesList.get(poolPropertiesList.size() - 1).setAppName(appName);
            return this;
        }

        public Builder initProcessNum(int initProcessNum) {
            check();
            poolPropertiesList.get(poolPropertiesList.size() - 1).setInitProcessNum(initProcessNum);
            return this;
        }

        public Builder startAndKillExistProcess(boolean startAndKillRunningProcess) {
            check();
            poolPropertiesList.get(poolPropertiesList.size() - 1).setStartAndKillExistProcess(startAndKillRunningProcess);
            return this;
        }

        public Builder fatalErrorTime(int fatalErrorTime) {
            check();
            poolPropertiesList.get(poolPropertiesList.size() - 1).setFatalErrorTime(fatalErrorTime);
            return this;
        }

        public Builder procedureImage(String procedureImage) {
            check();
            poolPropertiesList.get(poolPropertiesList.size() - 1).setProcedureImage(procedureImage);
            return this;
        }

        private void check() {
            if (poolPropertiesList.isEmpty()) {
                throw new RuntimeException("Use poolBegin before config pool");
            }
        }
    }

}
